/**
 * Элементы рисования для иктинуса.
 * 
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
 * @include "/ictinus/src/ictinus.js"
 */

ictinus.drawItems = (function() {

	/**
	 * Простой механизм наследования классов
	 * 
	 * @param {Function}
	 *            derived Наследуемый класс
	 * @param {Function}
	 *            from Базовый класс
	 */
	function inherit(derived, from) {
		var Inheritance = function() {
		};

		Inheritance.prototype = from.prototype;

		derived.prototype = new Inheritance();
		derived.prototype.constructor = derived;
		derived.baseConstructor = from;
		derived.superClass = from.prototype;
	};
	
	/**
	 * Обрезает число до 3 знаков после запятой
	 * @param {Number} num
	 */
	function crop(num) {
		return Math.round(num * 1000) / 1000;
	}
	
	/**
	 * Бегает по всем X и Y координатам объекта <code>item</code> и выполняет 
	 * на каждой координате функцию <code>func</code>, в которую передается 2 аргумента:
	 * <code>property</code> : String — название свойства, <code>type</code> : String — 
	 * тип свойства (x или y)
	 * @param {IcDrawItem} item
	 * @param {String|Array} x Массив X-координат объекта
	 * @param {String|Array} y Массив Y-координат объекта
	 * @param {Function} func Функция, выполняемая на каждом свойстве (<code>this</code> указывает на <code>item</code>). 
	 */
	function walkOnCoords(item, x, y, func) {
		x = (typeof(x) == 'string') ? x.split(',') : x;
		y = (typeof(y) == 'string') ? y.split(',') : y;
		
		for (var i = 0; i < x.length; i++) {
			func.call(item, x[i], 'x');
		}
		
		for (var i = 0; i < y.length; i++) {
			func.call(item, y[i], 'y');
		}
	}
	
	/**
	 * Расширяет объект <code>obj</code> с помощью свойств объекта <code>extender</code>
	 * @param {Object} obj
	 * @param {Object} extender
	 */
	function extend(obj, extender) {
		if (obj && extender)
			for (var p in extender) {
				obj[p] = extender[p];
				
			}
	}
	
	/** Ошибки, которые могут возникать при работе с элементами рисования */
	var errors = {
		no_prev: 'Этот элемент рисования не может быть первым в списке'
	};
	
	function throwError(type) {
		throw new Error(errors[type]);
	}

	/**
	 * @class Абстрактный элемент рисования
	 */
	function IcDrawItem() {
		/** @private */
		this._next = null;
		/** @private */
		this._prev = null;
	}

	IcDrawItem.prototype = {
		/**
		 * Преобразование элемента рисования к строке. Используется для экспорта
		 * в JSON
		 * 
		 * @return {String}
		 */
		toString : function() {
			return '';
		},

		/**
		 * Возвращает габаритный прямоугольник элемента рисования
		 */
		getBounds : function() {
			return {
				minX : 0,
				minY : 0,
				maxX : 0,
				maxY : 0
			};
		},

		/**
		 * Возвращает или устанавливает следующий элемент рисования
		 * 
		 * @param {IcDrawItem}
		 *            [item] Следующий элемент рисования в форме
		 * @return {ictinus.drawItems.Item}
		 */
		next : function(item) {
			if (arguments.length)
				this._next = item;

			return this._next || null;
		},

		/**
		 * Возвращает или устанавливает предыдущий элемент рисования
		 * 
		 * @param {IcDrawItem}
		 *            [item] Предыдущий элемент рисования в форме
		 * @return {ictinus.drawItems.Item}
		 */
		prev : function(item) {
			if (arguments.length)
				this._prev = item;

			return this._prev || null;
		},

		/**
		 * Клонирует текущий объект
		 * 
		 * @return {ictinus.drawItems.Item}
		 */
		clone : function() {
			return new this.constructor();
		},
		
		/**
		 * Проверяет, является ли текущий объект типом рисования <code>type</code>
		 * ('move', 'line', 'bezier')
		 * @param {String} Тип рисования
		 * @return {Boolean}
		 */
		typeOf: function(type){
			switch(type) {
				case 'move':
					return this.constructor == IcMove;
				case 'line':
					return this.constructor == IcLine;
				case 'bezier':
					return this.constructor == IcBezier;
			}
			
			return false;
		},
		
		/**
		 * Бегает по всем X и Y координатам объекта <code>item</code> и выполняет 
		 * на каждой координате функцию <code>func</code>, в которую передается 2 аргумента:
		 * <code>property</code> : String — название свойства, <code>type</code> : String — 
		 * тип свойства (x или y)
		 * @param {Function} func Функция, выполняемая на каждом свойстве (<code>this</code> указывает на <code>item</code>). 
		 */
		walkOnCoords: function(){
			
		},
		
		/**
		 * Рисует текущий путь на холсте (canvas)
		 * @param {CanvasRenderingContext2D} ctx
		 */
		drawCanvas: function(ctx){
			
		},
		
		/**
		 * Рисует текущий путь как VML
		 * @return {String}
		 */
		drawVML: function(){
//			alert(this.toString().toLowerCase());
			return this.toString().toLowerCase();
		}
	}

	/**
	 * Переместиться в указанную точку
	 * 
	 * @extends {IcDrawItem}
	 * @param {Number}
	 *            x
	 * @param {Number}
	 *            y
	 */
	function IcMove(x, y) {
		IcMove.baseConstructor.call(this);
		this.x = x;
		this.y = y;
	}
	
	inherit(IcMove, IcDrawItem);
	
	extend(IcMove.prototype, {
		clone : function() {
			return new this.constructor(this.x, this.y);
		},
		
		walkOnCoords: function(func){
			walkOnCoords(this, 'x', 'y', func);
		},
		
		drawCanvas: function(ctx){
			ctx.moveTo(this.x, this.y);
		}
	});
	
	// IE, гад, не хочет просто так получать функцию toString в extend()
	IcMove.prototype.toString = function() {
		return 'M' + crop(this.x) + ',' + crop(this.y);
	}
	
	/**
	 * Нарисовать линию в указанную точку
	 * 
	 * @extends {IcMove}
	 * @param {Number}
	 *            x
	 * @param {Number}
	 *            y
	 */
	function IcLine(x, y) {
		IcLine.baseConstructor.call(this);
		this.x = x;
		this.y = y;
	}
	
	inherit(IcLine, IcMove);
	
	extend(IcLine.prototype, {
		getBounds : function() {
			if (!this.prev())
				throwError(errors.no_prev);

			return {
				minX : Math.min(this.prev().x, this.x),
				minY : Math.min(this.prev().y, this.y),
				maxX : Math.max(this.prev().x, this.x),
				maxY : Math.max(this.prev().y, this.y)
			};
		},
		
		drawCanvas: function(ctx){
			ctx.lineTo(this.x, this.y);
		}
	});
	
	IcLine.prototype.toString = function() {
		return 'L' + crop(this.x) + ',' + crop(this.y);
	}
	

	/**
	 * Нарисовать кривую Безье в указанную точку
	 * 
	 * @class
	 * @extends {IcDrawItem}
	 * @param {Number}
	 *            x
	 * @param {Number}
	 *            y
	 * @param {Number}
	 *            cpx1
	 * @param {Number}
	 *            cpy1
	 * @param {Number}
	 *            cpx2
	 * @param {Number}
	 *            cpy2
	 * @param {Boolean}
	 *            locked
	 */
	function IcBezier(x, y, cpx1, cpy1, cpx2, cpy2, locked) {
		IcBezier.baseConstructor.call(this);
		this.x = x;
		this.y = y;
		this.cpx1 = cpx1;
		this.cpy1 = cpy1;
		this.cpx2 = cpx2;
		this.cpy2 = cpy2;
		this.locked = locked || false;
	}
	
	inherit(IcBezier, IcDrawItem);
	
	extend(IcBezier.prototype, {
		clone : function() {
			return new this.constructor(this.x, this.y, this.cpx1, this.cpy1,
					this.cpx2, this.cpy2, this.locked);
		},
		
		walkOnCoords: function(func){
			walkOnCoords(this, 'x,cpx1,cpx2', 'y,cpy1,cpy2', func);
		},

		getBounds : function() {
			
			/**
			 * @class
			 * Точка
			 * @param {Number} x
			 * @param {Number} y
			 */
			function Point(x, y) {
				/** @type {Number} */
				this.x = x;
				/** @type {Number} */
				this.y = y;
			}

			var solutions = [];

			/**
			 * Квадратное уравнение с отсечением решений вне (0,1)
			 * 
			 * @author Олег Коротаев (korotaev@design.ru)
			 * @param {Number} a
			 * @param {Number} b
			 * @param {Number} c
			 */
			function solveSq(a, b, c) {
				var g = b * b - 4 * a * c;

				if (a == 0) {
					solutions.push(-c / b);
					return;
				}

				if (g == 0) {
					solutions.push(-b / (2 * a));
				} else if (g > 0) {
					var d = Math.sqrt(b * b - 4 * a * c);

					if ((-b + d) / (2 * a) < 1 && (-b + d) / (2 * a) > 0)
						solutions.push((-b + d) / (2 * a));

					if ((-b - d) / (2 * a) < 1 && (-b - d) / (2 * a) > 0)
						solutions.push((-b - d) / (2 * a));
				}
			}

			/**
			 * Уравнение кривой Безье
			 * 
			 * @param {Number}
			 *            p0 Начальная точка
			 * @param {Number}
			 *            p1 Первая контрольная точка
			 * @param {Number}
			 *            p2 Вторая контрольная точка
			 * @param {Number}
			 *            p3 Конечная точка
			 * @param {Number}
			 *            t Time-итератор
			 * @return {Number} Координата для текущего time-итератора (<code>t</code>)
			 *         и точек
			 */
			function bezier(p0, p1, p2, p3, t) {
				return Math.pow(1 - t, 3) * p0 + 3 * t * Math.pow(1 - t, 2) * p1 + 3 * t * t * (1 - t) * p2 + t * t * t * p3;
			}

			return function() {
				
				if (!this.prev())
					throwError(errors.no_prev);
				
				var p0 = new Point(this.prev().x, this.prev().y);
				var p1 = new Point(this.cpx1, this.cpy1);
				var p2 = new Point(this.cpx2, this.cpy2);
				var p3 = new Point(this.x, this.y);
				 
				solutions = [];
				solutions.push(0); // начало кривой
				solutions.push(1); // конец кривой
				
				// производная кривой
				solveSq(
					-3 * p0.x + 9 * p1.x - 9 * p2.x + 3 * p3.x, 
					 6 * p0.x - 12 * p1.x + 6 * p2.x,
					-3 * p0.x + 3 * p1.x
				); 

				solveSq(
					-3 * p0.y + 9 * p1.y - 9 * p2.y + 3 * p3.y,
					 6 * p0.y - 12 * p1.y + 6 * p2.y,
					-3 * p0.y + 3 * p1.y
				);

				var maxx = -1, maxy = -1, minx = 1000, miny = 1000;

				for (var i = 0; i < solutions.length; i++) {
					var result;
					if (minx > (result = bezier(p0.x, p1.x, p2.x, p3.x, solutions[i])))
						minx = result;

					if (miny > (result = bezier(p0.y, p1.y, p2.y, p3.y, solutions[i])))
						miny = result;

					if (maxx < (result = bezier(p0.x, p1.x, p2.x, p3.x, solutions[i])))
						maxx = result;

					if (maxy < (result = bezier(p0.y, p1.y, p2.y, p3.y, solutions[i])))
						maxy = result;
				}

				return {
					minX : minx,
					minY : miny,
					maxX : maxx,
					maxY : maxy
				};
			}
		}(),
		
		drawCanvas: function(ctx){
			ctx.bezierCurveTo(this.cpx1, this.cpy1, this.cpx2, this.cpy2, this.x, this.y);
		},
		
		drawVML: function(){
			return this.toString().toLowerCase().replace('o', 'c');
		}
	});
	
	IcBezier.prototype.toString = function() {
		return (this.locked ? 'O' : 'C') + 
			crop(this.cpx1) + ',' + crop(this.cpy1) + ',' + 
			crop(this.cpx2) + ',' + crop(this.cpy2) + ',' + 
			crop(this.x) + ',' + crop(this.y);
	};

	return {
		Item: IcDrawItem,
		MoveTo: IcMove,
		LineTo: IcLine,
		BezierTo: IcBezier
	};

})();